/*
 * Copyright (c) 2007 The Chai Team
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.google.projects.chai.xmlrpc;

import com.google.projects.chai.data.HibernateUtil;
import com.google.projects.chai.data.User;
import com.google.projects.chai.util.Config;
import com.google.projects.chai.util.ErrorCode;
import com.google.projects.chai.util.Util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.xmlrpc.XmlRpcException;
import org.hibernate.Hibernate;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.exception.ConstraintViolationException;

/**
 * The UserHandler class of the Chai XML-RPC API. Non-static, non-void public methods
 * of this class are automatically exported as XML-RPC methods.
 * <p>
 * The UserHandler class contains methods relating to users, including 'static' methods
 * (like isUsernameAvailable), which don't require a Useras an argument, and
 * non-static methods (like addEmailAddress) which require a Useron which to
 * act.
 * <p>
 * Do not confuse this class with the similarly-named one in the data package.
 * That represents the object in our business logic and database systems. This
 * one is just a collection of related XML-RPC handler.
 * <p>
 * Also note that there may be more than one instance of this class at any given
 * time, or there may be a single instance being used concurrently to service
 * different XML-RPC requests. You should not try to maintain any request-,
 * session-, or object-related state in this class. Basically, you can't make
 * any guarantees about who else is or isn't using this class at the same time
 * or any other times. This is slightly more restrictive than is required by our
 * system, but it seems like a bad idea to store anything here.
 *
 * TODO these should all throw exceptions when they fail, even the simple 
 * boolean ones. It will probably be easier on the client that way.
 *
 * @author dap
 */

public class UserHandler {
        /*
         * "Static," unauthenticated methods
         * The following methods do not require a User object on which to act,
         * nor do they even require that a user be logged in in order to call
         * them.
         */
        
        /**
         * @param username a valid username
         * @return true only if the given user name is not already taken
         */
        public boolean isUsernameAvailable(String username) throws 
            XmlRpcException {
                if (username.trim().equals(""))
                        return (false);
                
                return (null == getUserByUsername(username));
        }
        
        /**
         * @param username a valid user name
         * @param password initial password for this user
         * @param email an email address, to be confirmed
         * @param real_name the user's real name
         * @param display_name the user's display name
         * @param location where the user indicates s/he lives.
         * @return the created user's id
         * @throws XmlRpcException E_USEREXISTS if the username is taken
         * @throws XmlRpcException E_BADVAL if the password is invalid
         */
        public int register(String username, String password,
            String email_address, String real_name, String display_name, 
            String location) throws XmlRpcException {
                if (!validatePassword(password))
                        throw (ErrorCode.E_BADVAL.getException());
                
                String confirmationHash = Util.generateConfirmationHash();
                
                User user = new User();
                user.setDisplayName(display_name.trim());
                user.setLocation(location.trim());
                user.setPrimaryEmail(email_address.trim());
                user.setRealName(real_name.trim());
                user.setUsername(username.trim());
                user.setPasswordHash(Util.hashPassword(password));
                user.setActive(false);
                user.setConfirmationHash(confirmationHash);
                
                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                
                try {
                        dbsession.save(user);
                        dbsession.getTransaction().commit();
                } catch (ConstraintViolationException e) {
                        dbsession.getTransaction().rollback();
                        throw (ErrorCode.E_USEREXISTS.getException());
                }
                
                Map<String, String> params = new HashMap<String, String>();
                params.put("User.ConfirmationHash", confirmationHash);
                params.put("User.DisplayName", user.getDisplayName());
                params.put("Site.Name", Config.getCurrent().getSiteName());
                Util.sendEmail(user.getPrimaryEmail(), "Registration", params);
                
                return (user.getUserIdentifier());
        }
        
        /*
         * "Static," authenticated methods.
         * These methods do not require a User object on which to act, but are
         * restricted to logged-in users.
         */
        
        /**
         * Searches for users by username
         * @param login_session a valid login session
         * @param username a valid username
         * @return a UserIdentifier object for the user with the given username, or null
         *      if none exists.
         * @throws XmlRpcException E_NOVAL if username is not specified
         * @throws InvalidLoginException if the login dbsession is not valid.
         */
        public int searchByUsername(String username) throws XmlRpcException {
                // TODO verify login
                username = username.trim();
                if ("".equals(username))
                        throw (ErrorCode.E_NOVAL.getException());
                User user = getUserByUsername(username);
                return (user == null ? -1 : user.getUserIdentifier());
        }

        /**
         * @param login_session a valid login session
         * @param email a valid email address
         * @return a UserIdentifier object for the user with the given email address, or
         *      null if none exists.
         * @throws InvalidLoginException if the login session is not valid.
         */
        public int searchByEmailAddress(String email) throws XmlRpcException {
                // TODO check login
                email = email.trim();
                if ("".equals(email))
                        throw (ErrorCode.E_NOVAL.getException());
                User user = getUserByEmailAddress(email);
                return (user == null ? -1 : user.getUserIdentifier());
        }
        
        /**
         * @param login_session a valid login session
         * @param name a user's name
         * @param location a user's location
         * @return a list of users who list themselves publicly and match the
         *      given criteria
         * @throws XmlRpcException E_NOVAL if neither name nor location is non-
         *      empty.
         * @throws InvalidLoginException if the login session is not valid.
         */
        public List<Integer> searchByNameAndLocation(String name,
            String location) throws XmlRpcException {
                // TODO check login
                location = location.trim();
                name = name.trim();
                
                StringBuffer where = new StringBuffer("from User where ");
                if (location.length() > 0 && name.length() > 0)
                        where.append("Location like ? and RealName like ?");
                else if (location.length() > 0)
                        where.append("Location like ?");
                else if (name.length() > 0)
                        where.append("RealName like ?");
                else
                        throw (ErrorCode.E_NOVAL.getException());
                
                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                Query query = dbsession.createQuery(where.toString());
                if (location.length() > 0 && name.length() > 0) {
                        query.setString(0, Util.makeSearchable(location));
                        query.setString(1, Util.makeSearchable(name));
                } else if (location.length() > 0) {
                        query.setString(0, Util.makeSearchable(location));
                } else {
                        query.setString(0, Util.makeSearchable(name));
                }
                
                List<User> users = query.list();
                dbsession.getTransaction().commit();
                
                List<Integer> ret = new ArrayList<Integer>(users.size());
                for (User usr : users)
                        ret.add(usr.getUserIdentifier());
                return (ret);
        }
        
        /*
         * Non-static, non-authenticated methods.
         * The following methods require a User object on which to act, but do
         * NOT require that any individual be logged in to use them.
         */
        
        /**
         * Resets a user's password and emails it to them.
         * TODO this is a security problem. People can search by username and
         * then reset other peoples' passwords.
         * @param username whose account to reset
         * @return 0 on success
         * @throws XmlRpcException E_NOSUCHUSER if the username is not valid
         */
        public int resetPasswordByUsername(String username) throws 
            XmlRpcException {
                User user = getUserByUsername(username.trim());
                if (user == null)
                        throw (ErrorCode.E_NOSUCHUSER.getException());
                resetPassword(user);
                
                return (0);
        }
        
        /**
         * Resets a user's password and emails it to them.
         * TODO this is a security problem. People can search by email address 
         * and then reset other peoples' passwords.
         * @param email_address the email address of the account to reset
         * @return 0 on success
         * @throws XmlRpcException E_NOSUCHUSER if the username is not valid
         */
        public int resetPasswordByEmailAddress(String email_address) throws 
            XmlRpcException {
                User user = getUserByEmailAddress(email_address.trim());
                if (user == null)
                        throw (ErrorCode.E_NOSUCHUSER.getException());
                resetPassword(user);
                
                return (0);
        }
        
        /*
         * Non-static, authenticated methods.
         * The following methods require an authenticated user and a User on 
         * which to act. These will nearly always be the same, but we could add
         * an administrator account who can change properties of other users.
         */
        
        /**
         * Associate an email address with the given user. Sends a confirmation
         * email to the new address and doesn't add the email address until it 
         * is confirmed.
         * @param session an authentication token
         * @param userId the user with which to assicate the new email address
         * @return true on success. There's no reason for this to fail (other 
         *      than the constant possibility of some underlying system failure).
         * @throws InvalidLoginException if the login session is not valid
         * @throws XmlRpcException E_NOSUCHUSER if the UserIdentifier is not valid
         */
        public int addEmailAddress(int userId, String email) throws 
            XmlRpcException {
                // TODO check login
                throw (ErrorCode.E_NOIMPL.getException());
        }
        
        /**
         * Confirm an email address. The confirmation code must match that
         * emailed to the given address.
         * @param session an authentication token
         * @param user the user whose address to confirm
         * @param email_address the email address to confirm
         * @param conf the confirmation string emailed to the address
         * @return true on success, false on failure (because the confirmation
         *      string doesn't match what was sent to the address or because
         *      the address is not pending confirmation)
         * @throws InvalidLoginException if the login session is not valid
         * @throws XmlRpcException E_NOSUCHUSER if the UserIdentifier is not valid
         */
        public int confirmEmailAddress(int userId, String email_address, 
            String conf) throws XmlRpcException {
                // TODO check login session
                throw (ErrorCode.E_NOIMPL.getException());
        }
        
        /**
         * Confirms a user's account. This is independent from confirming an
         * email address.
         *
         * @throws XmlRpcException E_NOSUCHUSER if the uid is not valid
         * @throws XmlRpcException E_BADVAL if the confirmation string does not
         *     match
         */
        public int confirmAccount(int uid, String confirmation) throws 
            XmlRpcException {
                User user = Util.getUserForIdentifier(uid);
                if (null == user)
                        throw (ErrorCode.E_NOSUCHUSER.getException());
                if (!user.getConfirmationHash().equals(confirmation))
                        throw (ErrorCode.E_BADVAL.getException());
                
                user.setActive(true);
                
                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                dbsession.update(user);
                dbsession.getTransaction().commit();
                
                return (0);
        }
        
        /**
         * Gets various properties of a user.
         * @param session an authentication token
         * @param user the user whose properties to get
         * @return user details
         * @throws InvalidLoginException if the login session is not valid
         * @throws XmlRpcException E_NOSUCHUSER if the user id is not valid
         */
        public Map<String, String> getProperties(int uid) throws 
            XmlRpcException {
                // TODO check login
                User user = Util.getUserForIdentifier(uid);
                if (null == user)
                        throw (ErrorCode.E_NOSUCHUSER.getException());
                        
                return (user.getPropertyMap());
        }
        
        /**
         * Set a particular named property (not including those which have their
         * own methods, like email addresses).
         * @param session an authentication token
         * @param user the user on which to set the properties
         * @return nothing
         * @throws InvalidLoginException if the login session is not valid
         * @throws IllegalArgumentException if the UserIdentifier is not valid
         * @throws IllegalArgumentException if the property doesn't exist, or
         *      if the value is invalid
         */
        public int setProperty(int user, String property, String value) throws 
            XmlRpcException {
                // TODO check login
                throw (ErrorCode.E_NOIMPL.getException());
        }
        
        /**
         * Dissociate an email address from the given user.
         * @param session an authentication token
         * @param user the user from which to dissociate the email address
         * @param email_address the email address to remove
         * @return nothing
         * @throws InvalidLoginException if the login session is not valid
         * @throws IllegalArgumentException if the UserIdentifier is not valid
         * @throws IllegalArgumentException if the user is not associated with
         *      the given email address
         */
        public int removeEmailAddress(int user, String email_address) throws 
            XmlRpcException {
                // TODO check login session
                throw (ErrorCode.E_NOIMPL.getException());
        }

        public int login(String username, String password) throws 
            XmlRpcException {
                // TODO implement me
                throw (ErrorCode.E_NOIMPL.getException());
        }
        
        /**
         * @return the user that corresponds to a given username
         */
        private User getUserByUsername(String username) {
                assert (username != null);

                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                List<User> users = dbsession.createQuery(
                    "from User where UserName = ?").
                    setString(0, username).list();
                dbsession.getTransaction().commit();

                assert (users.size() <= 1);
                return (users.isEmpty() ? null : users.get(0));
        }
        
        /** @return the user that corresponds to a given email address */
        private User getUserByEmailAddress(String email) {
                assert (email != null);
                
                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                List<User> users = dbsession.createQuery(
                    "from User where PrimaryEmail = ?").
                    setString(0, email).list();
                dbsession.getTransaction().commit();
                
                assert (users.size() <= 1);
                return (users.isEmpty() ? null : users.get(0));
        }
        
        /**
         * Deactivates a user and resets their password. Also sends them an 
         * email with a code that will grant them access again. This is done on 
         * resetPassword() as well as register().
         */
        private void resetPassword(User user) {
                assert (user != null);
                /* Generate a random confirmation string */
                String confirmationHash = Util.generateConfirmationHash();
                
                /* Make the DB changes (deactivating account, etc.) */
                Session dbsession = HibernateUtil.getCurrentSession();
                dbsession.beginTransaction();
                user.setActive(false);
                user.setConfirmationHash(confirmationHash);
                dbsession.update(user);
                dbsession.getTransaction().commit();
                
                /* Send the user an email */
                Map<String, String> params = new HashMap<String, String>();
                params.put("Site.Name", Config.getCurrent().getSiteName());
                params.put("User.DisplayName", user.getDisplayName());
                // TODO construct a working link which includes the random
                params.put("ConfirmationLink", "<no link>");
                Util.sendEmail(user.getPrimaryEmail(), "PasswordReset",
                    params);
        }

        /**
         * Returns true only if the given password is valid. The only invalid
         * password is currently the empty password. If users want to screw 
         * themselves, shouldn't we let them? (Or rather, if they want to leave
         * themselves open to be screwed, should we stop them?)
         */
        private boolean validatePassword(String password) {
                return (password.length() > 1);
        }
}
